﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using Beaker.Collections;
using Beaker.Injection;
using OpenCube.Nbt.Mapping.Attributes;
using OpenCube.Nbt.Mapping.Converters;

namespace OpenCube.Nbt.Mapping
{
	internal sealed class NbtMapper<TModel>
		: INbtMapper
		where TModel : class, new()
	{
		private readonly IBuildInConverterFactory _converterFactory;
		private readonly IQueryContainer _queryContainer;
		private readonly IReadOnlyList<PropertyMapping> _propertyMappings;

		public NbtMapper(IBuildInConverterFactory converterFactory, IQueryContainer queryContainer)
		{
			Contract.Requires<ArgumentNullException>(converterFactory != null);
			Contract.Requires<ArgumentNullException>(queryContainer != null);
			_converterFactory = converterFactory;
			_queryContainer = queryContainer;

			// collect all mapped properties on the model
			IEnumerable<PropertyMapping> mappings =
				from property in typeof(TModel).GetTypeInfo().GetDeclaredAndInheritedProperties()
				let nbtAttribs = property.GetNbtAttributes()
				where nbtAttribs.Any()
				let influentialAttribs = property.GetInfluentialAttributes()
				select new PropertyMapping(property, nbtAttribs.Concat(influentialAttribs));

			_propertyMappings = mappings.ToReadOnlyList();
		}

		INbtDictionary INbtMapper.MapToDictionary(object model)
		{
			return MapToDictionary((TModel)model);
		}

		object INbtMapper.MapToModel(INbtDictionary nbtDictionary)
		{
			return MapToModel(nbtDictionary);
		}

		public TModel MapToModel(INbtDictionary nbtDictionary)
		{
			var model = new TModel();

			foreach (PropertyMapping mapping in _propertyMappings)
			{
				// Get Value(s) from dictionary and make them ordered lists
				var values = new List<object>();
				var types = new List<Type>();

				foreach (string name in mapping.NbtTagNames)
				{
					object dictValue;
					if (nbtDictionary.TryGetValue(name, out dictValue))
					{
						values.Add(dictValue);
						types.Add(dictValue.GetType());
					}
					else
					{
						values.Add(null);
						types.Add(null);
					}
				}

				IReadOnlyList<object> nbtValues = values.ToReadOnlyList();
				IReadOnlyList<Type> nbtTypes = types.ToReadOnlyList();

				Type modelValueType = mapping.Property.PropertyType;

				// Create converter
				INbtTypeConverter converter = null;
				if (mapping.ConverterType != null)
				{
					converter = (INbtTypeConverter)_queryContainer.GetInstance(mapping.ConverterType);
					if (!converter.CanConvertToModel(nbtTypes, modelValueType))
						converter = null;
				}

				if (converter == null)
					converter = _converterFactory.Create(mapping.Property.PropertyType);

				if (!converter.CanConvertToModel(nbtTypes, modelValueType))
				{
					Debug.WriteLine("Cannot find a converter to convert from " + string.Join(", ", nbtTypes.Select(t => t == null ? "null" : t.Name).ToList()) + " to " + modelValueType.Name);

					var defaultValueAttribute = mapping.Attributes.OfType<DefaultValueAttribute>().FirstOrDefault();
					if( defaultValueAttribute != null )
						mapping.Property.SetValue(model, defaultValueAttribute.Value);
				}
				else
				{
					// use converter for conversion
					var context = new ConversionContext(_queryContainer, memberInfo: mapping.Property, nbtDictionary: nbtDictionary);
					object modelValue = converter.ConvertToModelValue(context, nbtValues);

					// Put converted value into model
					mapping.Property.SetValue(model, modelValue);
				}
			}

			return model;
		}

		public INbtDictionary MapToDictionary(TModel model)
		{
			throw new NotImplementedException();
		}

		private class PropertyMapping
		{
			private readonly PropertyInfo _property;
			private readonly IReadOnlyList<Attribute> _nbtAttributes;

			public PropertyMapping(PropertyInfo property, IEnumerable<Attribute> nbtAttributes)
			{
				Contract.Requires<ArgumentNullException>(property != null);
				Contract.Requires<ArgumentNullException>(nbtAttributes != null);

				_property = property;
				_nbtAttributes = nbtAttributes.ToReadOnlyList();
			}

			/// <summary>
			/// Gets the property to map
			/// </summary>
			public PropertyInfo Property
			{
				get { return _property; }
			}

			/// <summary>
			/// Gets the applied NbtAttributes that guide the mapping
			/// </summary>
			public IEnumerable<NbtAttribute> NbtAttributes
			{
				get { return _nbtAttributes.OfType<NbtAttribute>(); }
			}

			/// <summary>
			/// Gets all the applied Attributes that guide the mapping
			/// </summary>
			public IEnumerable<Attribute> Attributes
			{
				get { return _nbtAttributes; }
			}

			/// <summary>
			/// Gets the name(s) for the NBT Tag(s)
			/// </summary>
			public IReadOnlyList<string> NbtTagNames
			{
				get
				{
					NbtNameAttribute nameAttribute = NbtAttributes.OfType<NbtNameAttribute>().SingleOrDefault();

					if (nameAttribute != null)
						return nameAttribute.Names;

					return new ReadOnlyCollection<string>(new[] { _property.Name });
				}
			}

			public Type ConverterType
			{
				get
				{
					NbtConverterAttribute converterAttribute = NbtAttributes.OfType<NbtConverterAttribute>().FirstOrDefault();
					if (converterAttribute != null)
						return converterAttribute.ConverterType;

					return null;
				}
			}

		}
	}

	internal static class PropertyInfoExtensions
	{
		/// <summary>
		/// Get all the <see cref="NbtAttribute"/>s on the property.
		/// </summary>
		/// <param name="property">The property to get all the Nbt Attributes on.</param>
		/// <returns>A sequence of all the <see cref="NbtAttribute"/>s.</returns>
		public static IEnumerable<NbtAttribute> GetNbtAttributes(this PropertyInfo property)
		{
			return property.GetCustomAttributes().OfType<NbtAttribute>();
		}

		private static readonly IReadOnlyList<Type> InfluencingAttributes =
			new ReadOnlyCollection<Type>(
				new Type[]
				{
					typeof(DefaultValueAttribute)
				});

		/// <summary>
		/// Get all the attributes that are not NbtAttribute, but do influence the operation (like DefaultValue)
		/// </summary>
		/// <param name="property">The property to get all the Nbt Attributes on.</param>
		/// <returns>A sequence of all the  attributess.</returns>
		public static IEnumerable<Attribute> GetInfluentialAttributes(this PropertyInfo property)
		{
			return property.GetCustomAttributes().Where(a => InfluencingAttributes.Any(ia => ia == a.GetType()));
		}
	}

	public static class ReflectionExtensions
	{
		public static IEnumerable<PropertyInfo> GetDeclaredAndInheritedProperties( this TypeInfo typeInfo )
		{
			foreach (var property in typeInfo.DeclaredProperties)
				yield return property;

			Type baseType = typeInfo.BaseType;
			if (baseType != null)
			{
				foreach (var baseProperty in GetDeclaredAndInheritedProperties(baseType.GetTypeInfo()))
					yield return baseProperty;
			}
		}
	}
}